package com.google.code.b0rx0r.ui;

import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EventObject;
import java.util.List;

import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.GroupLayout;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.TransferHandler;
import javax.swing.GroupLayout.Alignment;
import javax.swing.border.BevelBorder;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

import org.jdesktop.swingx.JXCollapsiblePane;
import org.jdesktop.swingx.JXColorSelectionButton;
import org.jdesktop.swingx.JXHyperlink;
import org.jdesktop.swingx.JXStatusBar;
import org.jdesktop.swingx.JXTree;
import org.tritonus.share.sampled.FloatSampleBuffer;

import com.google.code.b0rx0r.B0rx0rGui;
import com.google.code.b0rx0r.ColorScheme;
import com.google.code.b0rx0r.PlayQueue;
import com.google.code.b0rx0r.program.Program;
import com.google.code.b0rx0r.program.ProgramManager;
import com.google.code.b0rx0r.program.ProgramManagerListener;
import com.google.code.b0rx0r.program.Sample;
import com.google.code.b0rx0r.program.SampleDataContainer;
import com.google.code.b0rx0r.program.Sequencable;
import com.google.code.b0rx0r.program.Sequence;
import com.google.code.b0rx0r.program.Slice;
import com.google.code.b0rx0r.program.Triggerable;
import com.google.code.b0rx0r.ui.model.ProgramTriggerableListTreeModel;



public class MainPane extends JPanel implements ProgramManagerListener {
	private static final String SLICER = "Slicer"; //$NON-NLS-1$
	private static final String SEQUENCER = "Sequencer"; //$NON-NLS-1$

	private ProgramManager programManager;
	private PlayQueue queue;
	
	private JXTree tLists;
	private JXStatusBar jxsbStatus = new JXStatusBar();
	private JLabel lHint = new JLabel();
	
	private JPanel pSlicerSequencer = new JPanel();
	private CardLayout clSlicerSequencer = new CardLayout();
	
	private SlicingPanel pSlicer;
	private SequencePanel pSequence = new SequencePanel(this);
	
	private Keyboard keyboard = new Keyboard();
	
	private JXCollapsiblePane jxcpKeyboard = new JXCollapsiblePane(JXCollapsiblePane.Orientation.VERTICAL);
	private JXCollapsiblePane jxcpLists = new JXCollapsiblePane(JXCollapsiblePane.Orientation.HORIZONTAL);
	
	private JButton bAddSample = new JButton(B0rx0rGui.createIcon("folder.gif")); //$NON-NLS-1$
	private JButton bAddSequence = new JButton(B0rx0rGui.createIcon("plus.gif")); //$NON-NLS-1$
	private JButton bRemove = new JButton(B0rx0rGui.createIcon("minus.gif")); //$NON-NLS-1$
	
	private JToggleButton tbLists = new JToggleButton(wrapAction(jxcpLists.getActionMap().get(JXCollapsiblePane.TOGGLE_ACTION), B0rx0rGui.createIcon("list_unordered.gif"))); //$NON-NLS-1$
	private JToggleButton tbKeyboard = new JToggleButton(wrapAction(jxcpKeyboard.getActionMap().get(JXCollapsiblePane.TOGGLE_ACTION), B0rx0rGui.createIcon("drop_box.gif"))); //$NON-NLS-1$
	
	private Previewable currentPreviewable = pSlicer;
	
	public MainPane(ProgramManager programManager, PlayQueue queue) {
		this.programManager = programManager;
		this.queue = queue;
		
		initGui();
		setHint(null);

		programManager.addProgramManagerListener(this);
		
		programChanged(null, programManager.getProgram());
		
	}

	@SuppressWarnings("serial") //$NON-NLS-1$
	private Action wrapAction(final Action action, Icon icon) {
		return new AbstractAction(null, icon) {
			@Override
			public void actionPerformed(ActionEvent e) {
				action.actionPerformed(e);
			}
		};
	}

	@Override
	public void programChanged(Program oldProgram, Program newProgram) {
		tLists.expandAll();
	}

	public void setHint(String hint) {
		if (hint == null)
			lHint.setText(Messages.getString("MainPane.HintReady")); //$NON-NLS-1$
		else
			lHint.setText(hint);
	}


	private void initGui() {

		pSlicer = new SlicingPanel(programManager, queue);
		
		JPanel pTop = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
		pTop.add(tbLists);
		pTop.add(tbKeyboard);

		JPanel pMain = new JPanel(new BorderLayout());
		pMain.add(pSlicerSequencer, BorderLayout.CENTER);
		pMain.add(jxcpKeyboard, BorderLayout.SOUTH);
		pMain.add(jxcpLists, BorderLayout.WEST);
		
		setLayout(new BorderLayout());
		add(pTop, BorderLayout.NORTH);
		add(pMain, BorderLayout.CENTER);
		add(jxsbStatus, BorderLayout.SOUTH);
		
		
		jxsbStatus.add(lHint, new JXStatusBar.Constraint(JXStatusBar.Constraint.ResizeBehavior.FILL));
		
		
		tbLists.setSelected(true);
		tbKeyboard.setSelected(true);
		
		TreeModel tm = new ProgramTriggerableListTreeModel(programManager);
		tm = new SortedTreeModel(tm, new Comparator<Object>() {

			@Override
			public int compare(Object o1, Object o2) {
				if (o1 == o2)
					return 0;
				
				if (o1 == null)
					return 1;
				
				if (o2 == null)
					return -1;
				
				return o1.toString().compareTo(o2.toString());
			}
			
		});
		
		tLists = new JXTree(tm);
		tLists.setRootVisible(false);
		tLists.setCellRenderer(new TriggerableTreeCellRenderer());
		tLists.setCellEditor(new TriggerableTreeCellEditor());
		tLists.setEditable(true);
		
	
		
		JScrollPane spLists = new JScrollPane(tLists, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

		tLists.setDragEnabled(true);
		tLists.setTransferHandler(new TransferHandler() {

			/**
			 * 
			 */
			private static final long serialVersionUID = 1L;
			@Override
			public int getSourceActions(JComponent c) {
				return DnDConstants.ACTION_MOVE;
			}

			@Override
			protected Transferable createTransferable(JComponent c) {
				final Object o = tLists.getSelectionPath().getLastPathComponent();
				
				if (o instanceof Sequencable) 
					return new Transferable() {
						@Override
						public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
							return o;
						}

						@Override
						public DataFlavor[] getTransferDataFlavors() {
							return new DataFlavor[]{Sequencable.DATA_FLAVOR, Triggerable.DATA_FLAVOR};
						}

						@Override
						public boolean isDataFlavorSupported(DataFlavor flavor) {
							return flavor.equals(Sequencable.DATA_FLAVOR) ||
							       flavor.equals(Triggerable.DATA_FLAVOR)
							;
						}
					
				};
				
				return super.createTransferable(c);
			}

			private final Icon icon = B0rx0rGui.createIcon("table.gif"); //$NON-NLS-1$
			@Override
			public Icon getVisualRepresentation(Transferable t) {
				return icon;
			}
		
		});
		
		
		JPanel pLists = new JPanel();
		GroupLayout layLists = new GroupLayout(pLists);
		pLists.setLayout(layLists);
		
		layLists.setHorizontalGroup(layLists.createParallelGroup()
				                           .addComponent(spLists)
				                           .addGroup(layLists.createSequentialGroup()
				                          		 .addComponent(bAddSample)
				                          		 .addComponent(bAddSequence)
				                          		 .addComponent(bRemove)
				                          		 )
				                           );
		
		layLists.setVerticalGroup(layLists.createSequentialGroup()
        .addComponent(spLists)
        .addGroup(layLists.createParallelGroup()
       		 .addComponent(bAddSample)
       		 .addComponent(bAddSequence)
       		 .addComponent(bRemove)
       		 )
        );
		

		jxcpLists.add(pLists);
		jxcpLists.setBorder(BorderFactory.createEtchedBorder(BevelBorder.LOWERED));
		
		jxcpKeyboard.add(keyboard);
		jxcpKeyboard.setBorder(BorderFactory.createEtchedBorder(BevelBorder.LOWERED));
		
		
		pSlicerSequencer.setLayout(clSlicerSequencer);
		pSlicerSequencer.add(pSlicer, SLICER);
		pSlicerSequencer.add(pSequence, SEQUENCER);
		pSlicerSequencer.setBorder(BorderFactory.createEtchedBorder(BevelBorder.LOWERED));
		
		
		
		if (queue != null) {
			queue.addLearner(keyboard);
		}
		
		
		
		
		
		
		
		bAddSample.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent arg0) {
				// display file selection dialog
				JFileChooser jfc = new JFileChooser();
				jfc.setAcceptAllFileFilterUsed(true);
				jfc.setFileFilter(new FileNameExtensionFilter(Messages.getString("MainPane.FileFilter.Samples"), "wav", "mp3")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				
				jfc.showOpenDialog(MainPane.this);

				
				// if not cancelled, create sample from file and add it to program
				File f = jfc.getSelectedFile();
				if (f != null) {
					try {
						Sample s = new Sample(f);
						programManager.getProgram().addSample(s);
					} catch (UnsupportedAudioFileException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (LineUnavailableException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				
			}
		});
	  
		
		bAddSequence.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent arg0) {
				Sequence s = new Sequence(programManager);
				programManager.getProgram().addSequence(s);
				pSequence.setSequence(s);
				clSlicerSequencer.show(pSlicerSequencer, SEQUENCER);
				currentPreviewable = pSequence;
			}
		});

  	GlobalHotkeyManager.getInstance().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "preview"); //$NON-NLS-1$
		GlobalHotkeyManager.getInstance().getActionMap().put("preview", new AbstractAction() { //$NON-NLS-1$

			@Override
			public void actionPerformed(ActionEvent e) {
				currentPreviewable.triggerPreview(queue);
			}
			
		});
		
		tLists.addTreeSelectionListener(new TreeSelectionListener() {

			@Override
			public void valueChanged(TreeSelectionEvent e) {
				TreePath tp = e.getNewLeadSelectionPath();
				if (tp != null) {
					Object tip = tp.getLastPathComponent();
					if (tip != null) {
						if (tip instanceof Slice) {
							Slice s = (Slice)tip;
							pSlicer.setSample(s.getSample());
							pSlicer.setSelectedSlice(s);
						} 
						
						else if (tip instanceof Sample) {
							Sample s = (Sample)tip;
							pSlicer.setSample(s);
							
							clSlicerSequencer.show(pSlicerSequencer, SLICER);
							currentPreviewable = pSlicer;
							
//							pSlicer.setSelectedSlice(null);
						} 
						
						else if (tip instanceof Sequence) {
							Sequence s = (Sequence)tip;
							pSequence.setSequence(s);
							
							clSlicerSequencer.show(pSlicerSequencer, SEQUENCER);
							currentPreviewable = pSequence;
						}

					}
				}
				// TODO Auto-generated method stub
				
			}
			
		});
		
		pSlicer.addTriggerableSelectionListener(new TriggerableSelectionListener() {

			@Override
			public void selectionChanged(Triggerable selected) {
				TreePath tp = findInTree(selected, tLists);
				tLists.setSelectionPath(tp);
			}

			private TreePath findInTree(Triggerable needle, JXTree haystack) {
				Object root = haystack.getModel().getRoot();
				TreePath tpCurrent = new TreePath(root);
				
				return findInTree(needle, haystack.getModel(), tpCurrent);
			}

			private TreePath findInTree(Triggerable needle, TreeModel haystack, TreePath path) {
				if (path.getLastPathComponent() == needle) {
					return path;
				}
				
				Object last = path.getLastPathComponent();
				for (int i = 0; i<haystack.getChildCount(last); i++) {
					Object next = haystack.getChild(last, i);
					TreePath result = findInTree(needle, haystack, path.pathByAddingChild(next));
				
					if (result != null)
						return result;
				}
				
				return null;
			}
			
		});
		
	}


	private static class TriggerableTreeCellEditor implements TreeCellEditor {
    private JPanel pEditor = new JPanel();
    private JTextField tfLabel = new JTextField();
    private JXColorSelectionButton cbColor = new JXColorSelectionButton();
    private JXHyperlink bOk = new JXHyperlink();
    private JXHyperlink bCancel = new JXHyperlink();
    {
    	pEditor.setOpaque(false);
    	cbColor.setPreferredSize(new Dimension(48, 24));
    	
    	GroupLayout gl = new GroupLayout(pEditor);
    	pEditor.setLayout(gl);
    	gl.setAutoCreateGaps(true);
    	
    	gl.setHorizontalGroup(gl.createSequentialGroup()
    			.addComponent(tfLabel, GroupLayout.PREFERRED_SIZE, 200, 200)
    			.addComponent(cbColor)
    			.addComponent(bOk)
    			.addComponent(bCancel)
    			);
    
    	gl.setVerticalGroup(gl.createParallelGroup(Alignment.CENTER)
    			.addComponent(tfLabel)
    			.addComponent(cbColor,GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
    			.addComponent(bOk)
    			.addComponent(bCancel)
    			);    	
    	
    	Action aOk = new AbstractAction(null, B0rx0rGui.createIcon("tick.gif")) { //$NON-NLS-1$
				@Override
				public void actionPerformed(ActionEvent e) {
					stopCellEditing();
				}
    	};
    	
    	Action aCancel = new AbstractAction(null, B0rx0rGui.createIcon("cross.gif")) { //$NON-NLS-1$
				@Override
				public void actionPerformed(ActionEvent e) {
					cancelCellEditing();
				}
    	};    	
    	
    	/* i dont have the faintest idea why i can set the escape-keystroke directly but have to jump through hoops for the enter keystroke. */
    	tfLabel.getActionMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), aCancel);
    	cbColor.getActionMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), aCancel);

    	tfLabel.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "enter"); //$NON-NLS-1$
    	tfLabel.getActionMap().put("enter", aOk); //$NON-NLS-1$
    	
    	cbColor.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "enter"); //$NON-NLS-1$
    	cbColor.getActionMap().put("enter", aOk); //$NON-NLS-1$

    	bOk.setAction(aOk);
    	bCancel.setAction(aCancel);
    	
    	
    }

    private Triggerable currentlyEditing = null;
		@Override
		public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) {
			if (value instanceof Triggerable) {
				currentlyEditing = (Triggerable) value;
				
				pEditor.setSize(tree.getWidth(), tree.getRowHeight());
				
				tfLabel.setText(currentlyEditing.getName());
				cbColor.setBackground(currentlyEditing.getColor());
				
				return pEditor;
			}
			
			return null;
		}

		@Override
		public void cancelCellEditing() {
			currentlyEditing = null;
			
			fireEditingCancelled();
		}

		private void fireEditingCancelled() {
			ChangeEvent e = new ChangeEvent(this);
			for(CellEditorListener l : listeners) {
				l.editingCanceled(e);
			}
		}

		private void fireEditingStopped() {
			ChangeEvent e = new ChangeEvent(this);
			for(CellEditorListener l : listeners) {
				l.editingStopped(e);
			}
		}
		@Override
		public boolean stopCellEditing() {
			Triggerable t = currentlyEditing;
			if (t != null) {
			  t.setName(tfLabel.getText());
			  t.setColor(cbColor.getBackground());
			}
			
			currentlyEditing = null;
			
			fireEditingStopped();
			
			return true;
		}

		@Override
		public boolean isCellEditable(EventObject anEvent) {
			Object o = anEvent.getSource();
			
			if (o instanceof JXTree) {
			  o = ((JXTree)o).getLastSelectedPathComponent();	
			}
			
			return o instanceof Triggerable;
		}

		@Override
		public Object getCellEditorValue() {
			return currentlyEditing;
		}


		private List<CellEditorListener> listeners = new ArrayList<CellEditorListener>();
		@Override
		public void addCellEditorListener(CellEditorListener l) {
			listeners.add(l);
		}

		@Override
		public void removeCellEditorListener(CellEditorListener l) {
      listeners.remove(l);			
		}

		@Override
		public boolean shouldSelectCell(EventObject anEvent) {
			return true;
		}

	}
  private static class TriggerableTreeCellRenderer implements TreeCellRenderer {
    private DefaultTreeCellRenderer dtcr = new DefaultTreeCellRenderer();
    
		SimpleWaveformDisplay swd = new SimpleWaveformDisplay(300, 24);
		JLabel lSwd = new JLabel(); 
		{
			lSwd.setFont(lSwd.getFont().deriveFont(Font.BOLD));
			lSwd.setHorizontalAlignment(JLabel.LEFT);
			lSwd.setVerticalAlignment(JLabel.CENTER);
			swd.setLayout(new BorderLayout());
			swd.add(lSwd, BorderLayout.CENTER);
		}
		
		JLabel lTitle = new JLabel();
		{
			lTitle.setFont(lTitle.getFont().deriveFont(Font.BOLD));
			lTitle.setFont(lTitle.getFont().deriveFont(14f));

		}
		
		@Override
		public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {

			if (!(value instanceof Triggerable)) {
          lTitle.setText(value.toString());
          return lTitle;
			}

			lSwd.setOpaque(false);
			
			Triggerable s = (Triggerable) value;

			if (value instanceof SampleDataContainer) {
				SampleDataContainer sdc = (SampleDataContainer)value;
				
			  FloatSampleBuffer fsb = sdc.getFloatSampleBuffer();
		    swd.setData(fsb, sdc.getStartOffset(), sdc.getEndOffset() - sdc.getStartOffset());
//			} else {
//				swd.setData(null, 0, 0);
			}

			
		  Color color = s.getColor();
		  Color bgSelected = color == null ? dtcr.getBackgroundSelectionColor() : color;
		  Color bgUnselected = color == null ? tree.getBackground() : new Color(color.getRed(), color.getGreen(), color.getBlue(), 100); 
		  
			Color back = selected ? bgSelected : bgUnselected;
			Color fore = tree.getForeground();
			
			swd.setBackground(back);
			swd.setForeground(ColorScheme.TREE_WAVEFORM_COLOR);

			lSwd.setText(s.getName());
			lSwd.setForeground(fore);
			
			return swd;
		}

		
	}

}
